/**
 * @property {number} speed 动画持续时间（毫秒）
 * @property { number } defaultIndex 初始显示的轮播图的索引
 * @property { number } auto 自动播放的时间间隔（毫秒）
 * @property { continuous }
 */
interface CarouselIntf {
  /**
   * 动画持续时间（毫秒），默认为 300
   */
  speed?: number;
  /**
   * 初始显示的轮播图的索引，默认为：0
   */
  defaultIndex?: number;
  /**
   * 自动播放的时间间隔（毫秒），默认为 3000，如果设置小于等于0，则停止自动播放
   */
  auto?: number;
  /**
   * 是否可以循环播放，默认：true
   */
  continuous?: boolean;
  /**
   * 是否显示 indicators，默认：true
   */
  showIndicators?: boolean;
  /**
   * 点击指示小圆点时，是否进行切换，默认为：false
   */
  indicatorToggle?: boolean;
  /**
   * 在只有一页时不允许滑动，默认：true
   */
  noDragWhenSingle?: boolean;
  /**
   * 是否在 touchstart 事件触发时阻止事件的默认行为。设为 true 可提高运行在低版本安卓浏览器时的性能，默认：true
   */
  prevent?: boolean;
  /**
   * 是否在 touchstart 事件触发时阻止冒泡。默认：true
   */
  stopPropagation?: boolean;
  /**
   * 容器选择器
   */
  el?: string;
}

interface CarouseConf {
  speed: number;
  defaultIndex: number;
  auto: number;
  continuous: boolean;
  showIndicators: boolean;
  noDragWhenSingle: boolean;
  prevent: boolean;
  stopPropagation: boolean;
  indicatorToggle: boolean;
  el: any;
}

interface Point {
  pageX: number;
}

const isActive = 'ph-swipe-item-active';
const dotActive = 'ph-swipe-indicator-active';
const vendorPrefix = ['', 'webkit', 'moz', 'ms'];

// 扩展函数
let assign = Object.assign || function (target: any, ...source: any) {
  let from
  let to = Object(target)

  for (let s = 1; s < arguments.length; s++) {
    from = Object(arguments[s])

    for (let key in from) {
      if (from.hasOwnProperty(key)) {
        to[key] = from[key]
      }
    }
  }
  return to
}

/**
 * 根据选择器获取节点
 * @param {string} selector 选择器
 */
function $ (selector: string, dom?: any) {
  return (dom || document).querySelectorAll(selector);
}

/**
 * 为节点添加 class
 * @param {HTMLElement} elem 待添加 class 的节点
 * @param {string} clazz     需要添加的 class
 */
function addClass (elem: any, clazz: string) {
  elem.classList.add(clazz);
}

/**
 * 节点移除 class
 * @param {HTMLElement} elem 待移除 class 的节点
 * @param {string} clazz     需要移除的 class
 */
function removeClass (elem: any, clazz: string) {
  elem.classList.remove(clazz);
}

/**
 * 为节点添加 transition 属性，包括浏览器前缀
 * @param {HTMLElement} element 需要添加 css Transition 属性的节点
 * @param {string}      value   css transition 值
 */
function transition (element: any, value: string) {
  vendorPrefix.forEach(prefix => {
    const t = prefix === '' ? 'transition' : (prefix + 'Transition');
    element.style[t] = value;
  })
}

/**
 * 为节点添加 transform 属性，包括浏览器前缀
 * @param {HTMLElement} element  需要添加 css transform 属性的节点
 * @param {string}      value     css transform 值
 */
function transform (element: any, value: string) {
  vendorPrefix.forEach(prefix => {
    const t = prefix === '' ? 'transform' : (prefix + 'Transform');
    element.style[t] = value;
  })
}

/**
 * 为节点添加只执行一次的事件处理
 * @param {HTMLElement} element   添加只执行一次事件的节点
 * @param {string}      listener  事件名称
 * @param {function}    event     事件处理函数
 */
function once(element: any, listener: string, event: () => void) {
  element.addEventListener(listener, event, { once: true });
}

let defaultConfig: CarouseConf = {
  speed: 300,
  defaultIndex: 0,
  auto: 3000,
  continuous: true,
  showIndicators: true,
  noDragWhenSingle: true,
  prevent: true,
  stopPropagation: true,
  indicatorToggle: false,
  el: ''
}

class Carousel {
  private _config: CarouseConf;
  private _timer: any;
  private _length: number; // 滑动页面数量
  private _index: number; // 当前页面的 index
  private _dragging: boolean;
  private _animating: boolean;
  private _noDrag: boolean; // 是否禁止滑动
  private _pages: NodeList;
  private _el: any;
  private _dots: HTMLDivElement[];
  private _dragState: any;
  private _dotDonw: boolean;

  public constructor(config: CarouselIntf) {
    this._timer = null;
    this._length = 3;
    this._index = 0;
    this._dragging = false;
    this._animating = false;
    this._noDrag = false;
    this._dotDonw = false;
    this._dragState = {};
    this._dots = [];
    this._pages = $('.ph-swipe');
    this._el = $('.ph-swipe')[0];
    this._config = assign(defaultConfig, config || {});
    if (this._config.el === null || this._config.el === undefined) {
      throw new Error('请配置正确的容器节点！');
    } else {
      let $el = $(this._config.el)[0];
      if ($el === null || $el === undefined) {
        throw new Error('请配置正确的容器节点！');
      } else {
        this._reInitPages(); // 初始化页面
        this._initIndicators(); // 初始化圆点指示器
        this._initTimer(); // 初始化自动轮播
        this._initEvent(); // 初始化事件处理
      }
    }
  }

  /**
   * 滑动到下一页
   */
  protected next() {
    this._doAnimate('next');
  }

  /**
   * 加载页面相关配置
   */
  private _reInitPages() {
    const $items = $('.ph-swipe-item');
    const itemsLen = $items.length;
    this._length = itemsLen;
    this._noDrag = itemsLen === 1 && this._config.noDragWhenSingle;
    const indeDefaultIndex = Math.floor(this._config.defaultIndex);
    const defaultIndex = (indeDefaultIndex >= 0 && indeDefaultIndex < itemsLen) ? indeDefaultIndex : 0;
    this._index = defaultIndex;
    for (let i = 0; i < itemsLen; i++) {
      let child = $items[i];

      removeClass(child, isActive);

      if (i === defaultIndex) {
        addClass(child, isActive);
      }
    }
    this._pages = $items;
  }

  private _initIndicators() {
    if (this._config.showIndicators && this._length > 1) {
      let dots = [];
      let $dots = document.createElement('div');
      $dots.className = 'ph-swipe-indicators';
      for (let i = 0; i < this._length; i++) {
        let clazz = 'ph-swipe-indicator'
        if (this._index === i) {
          clazz += ' ' + dotActive;
        }
        let $dot = document.createElement('div');
        $dot.className = clazz;
        $dot.setAttribute('data-index', String(i));
        $dots.appendChild($dot);
        dots.push($dot);
      }
      if (this._config.indicatorToggle) {
        $dots.addEventListener('touchstart', (e: any) => { this.dotClick(e) });
        $dots.addEventListener('mousedown', (e: any) => { this.dotClick(e) });
        $dots.addEventListener('touchend', (event: any) => { this.dotClickRestore(event) });
        $dots.addEventListener('mouseup', (event: any) => { this.dotClickRestore(event) });
      }
      this._el.appendChild($dots);
      this._dots = dots;
    }
  }

  /**
   * 初始化自动轮播
   */
  private _initTimer() {
    if (this._config.auto > 0 && !this._timer) { // 自动播放
      // 定时滑动到下一页
      this._timer = setInterval(() => {
        // 如果配置了不循环滑动，并且已经滑动到末尾了，则停止滑动
        if (!this._config.continuous && this._index >= this._length - 1) {
          this._clearTimer()
        }
        // 如果当前不是正在手动滑动和处于滑动动画中，则滑动到下一页
        if (!this._dragging && !this._animating) {
          this.next();
        }
      }, this._config.auto)
    }
  }

  private dotClickRestore (event: any) {
    event.preventDefault();
    event.stopPropagation();
    if (this._dotDonw) {
      this._initTimer();
      this._dotDonw = false;
    }
  }

  private dotClick (event: any) {
    event.preventDefault();
    event.stopPropagation();
    let i = event.target.getAttribute('data-index');
    if (i) {
      let index = Number(i);
      if (index !== this._index) { // 切换
        this._dotDonw = true;
        this._clearTimer(); // 清楚自动轮播定时器
        let towards = index > this._index ? 'next' : 'prev';
        let pageWidth = this._el.clientWidth;
        let prevPage: any;
        if (towards === 'prev') {
          prevPage = this._pages[index];
          prevPage.style.display = 'block'; // 显示上一页
          this._translate(prevPage, -pageWidth); // 执行 css 滑动动画
        }
        let nextPage: any;
        if (towards === 'next') {
          nextPage = this._pages[index];
          nextPage.style.display = 'block';
          this._translate(nextPage, pageWidth);
        }
        this._doAnimate(towards, {
          currentPage: this._pages[this._index],
          newIndex: index,
          prevPage: prevPage,
          nextPage: nextPage,
          raf: false,
          pageWidth: pageWidth,
          dot: true
        })
      }
    }
  }

  private _clickEvent (event: Event) {
    if (this._config.prevent) event.preventDefault();
    if (this._config.stopPropagation) event.stopPropagation();
    if (this._animating) return;
    this._dragging = true;
  }

  private _endEvent () {
    this._initTimer(); // 手动滑动结束后，重新开启自动滑动
    this._dragging = false;
    this._dragState = {};
  }

  private _restoreDom () {
    let dragState = this._dragState;
    if (dragState.prevPage) {
      dragState.prevPage.style.display = ''
    }
    if (dragState.nextPage) {
      dragState.nextPage.style.display = ''
    }
  }

  /**
   * 初始化事件处理
   */
  private _initEvent () {
    const element: Element = this._el;

    // 当手指触摸屏幕时候触发
    element.addEventListener('touchstart', (event: any) => {
      this._clickEvent(event);
      this._touchStart({ pageX: event.touches[0].pageX });
    });

    // 当手指在屏幕上滑动的时候连续地触发。
    element.addEventListener('touchmove', (event: any) => {
      if (!this._dragging) return;
      if (this._timer) this._clearTimer();
      this._touchMove({ pageX: event.touches[0].pageX });
    });

    // 当系统停止跟踪触摸的时候触发。
    element.addEventListener('touchend', (event: any) => {
      if (!this._dragging) return;
      this._touchEnd();
      this._endEvent();
    });

    // 鼠标按下事件
    element.addEventListener('mousedown', (event: any) => {
      this._clickEvent(event);
      this._touchStart({ pageX: event.pageX })
    });

    // 鼠标滑动事件
    element.addEventListener('mousemove', (event: any) => {
      if (!this._dragging) return;
      if (this._timer) this._clearTimer();
      this._touchMove({ pageX: event.pageX });
    });

    // 鼠标释放事件
    element.addEventListener('mouseup', () => {
      if (!this._dragging) return;
      this._touchEnd();
      this._endEvent();
    });
  }

  /**
   * 清楚定时任务
   */
  private _clearTimer() {
    clearInterval(this._timer)
    this._timer = null
  }

  private _doAnimate (towards: string|null, options?: any) {
    if (this._pages.length < 2) return;
    let pages: NodeList = this._pages;
    let pageCount: number = this._length;
    let pageWidth: number;
    let index: number = this._index;
    let speed: number = this._config.speed || 300; // 动画持续时间

    let prevPage: any;
    let currentPage: any;
    let nextPage: any;
    let raf = false;
    let offsetLeft: number;
    let newIndex = -1;

    if (!options) { // 自动滑动
      pageWidth = this._el.clientWidth;
      currentPage = pages[index];
      if (index > 0) {
        prevPage = pages[index - 1];
      }
      if (index < pageCount - 1) {
        nextPage = pages[index + 1];
      }

      // 循环滑动
      if (this._config.continuous) {
        // 当前是首页的时候，上一页为末尾页
        if (index === 0) {
          prevPage = pages[pageCount - 1];
        }
        // 如果当前是末尾页的时候，下一页为首页
        if (index === pageCount - 1) {
          nextPage = pages[0];
        }
      }

      if (prevPage) {
        /* 显示上一页并且将上一页通过 transform 的方式显示到最左边 */
        prevPage.style.display = 'block'; // 显示上一页
        this._translate(prevPage, -pageWidth); // 执行 css 滑动动画
      }
      if (nextPage) {
        nextPage.style.display = 'block';
        this._translate(nextPage, pageWidth);
      }
    } else {
      newIndex = options.newIndex;
      if (newIndex === null || newIndex === undefined) {
        newIndex = -1;
      }
      prevPage = options.prevPage;
      currentPage = options.currentPage;
      nextPage = options.nextPage;
      raf = options.raf;
      offsetLeft = options.offsetLeft;
      pageWidth = options.pageWidth;

    }

    let oldPage: Node = pages[index];

    if (newIndex === -1) {
      if (towards === 'prev') { // 上一页
        if (index > 0) {
          newIndex = index - 1;
        }
        if (this._config.continuous && index === 0) {
          newIndex = pageCount - 1;
        }
      } else if (towards === 'next') { // 下一页
        if (index < pageCount - 1) {
          newIndex = index + 1;
        }
        if (this._config.continuous && index === pageCount - 1) {
          newIndex = 0;
        }
      }
    }

    // 动画完成后的回调
    let callback = () => {
      if (newIndex !== -1) {
        let newPage: any = this._pages[newIndex];

        removeClass(oldPage, isActive); // 隐藏之前显示的页面
        addClass(newPage, isActive); // 显示当前的页面

        // 切换圆点指示器
        if (this._config.showIndicators && this._length > 1) {
          removeClass(this._dots[index], dotActive);
          addClass(this._dots[newIndex], dotActive);
        }

        this._index = newIndex;
      }

      // 动画完成后，隐藏之前的上一页和下一页的节点
      if (prevPage) {
        prevPage.style.display = '';
      }
      if (nextPage) {
        nextPage.style.display = '';
      }
    }

    // 执行滑动动画
    setTimeout(() => {
      if (towards === 'next') { // 下一页
        if (raf) {
          this._rafTranslate(currentPage, offsetLeft, pageWidth, callback, nextPage);
        } else {
          // 执行滑动的 css 动画
          this._translate(currentPage, -pageWidth, speed, callback); // 当前页面滑动到最左端
          if (nextPage) {
            this._translate(nextPage, 0, speed); // 将下一页滑动到显示区域
          }
        }
      } else if (towards === 'prev') { // 上一页
        if (raf) {
          this._rafTranslate(currentPage, offsetLeft, -pageWidth, callback, prevPage);
        } else {
          this._translate(currentPage, pageWidth, speed, callback);
          if (prevPage) {
            this._translate(prevPage, 0, speed);
          }
        }
      } else { // 滑动的距离低于一定距离，回复到原来的页面
        this._translate(currentPage, 0, speed, callback);
        if (prevPage) {
          this._translate(prevPage, -pageWidth, speed);
        }
        if (nextPage) {
          this._translate(nextPage, pageWidth, speed);
        }
      }
    }, 10)
  }

  /**
   * 手动滑动后，剩余的部分，用 js 通过 requestAnimationFrame 动画来进行滑动
   */
  private _rafTranslate (element: any, initOffset: number, offset: number, callback?: () => void, nextElement?: any) {
    let raf = 0;
    let start = 0;
    let _offset = initOffset;
    let animationLoop = (timestamp: number) => {
      if (start === 0) start = timestamp;
      let progress = timestamp - start;
      progress = Math.floor(progress / 3);
      _offset = initOffset > 0 ? (_offset + progress) : (_offset - progress);
      if (Math.abs(_offset) < Math.abs(offset)) {
        transform(element, `translate3d(${_offset}px, 0, 0)`);
        if (nextElement) {
          transform(nextElement, `translate3d(${_offset + offset}px, 0, 0)`);
        }
        raf = window.requestAnimationFrame(animationLoop);
      } else {
        transform(element, '');
        if (nextElement) {
          transform(nextElement, '');
        }
        window.cancelAnimationFrame(raf);
        if (callback) {
          callback();
        }
      }
    }

    raf = window.requestAnimationFrame(animationLoop);
  }

  /**
   * 用于自动滑动时，执行 css 动画
   * @param {Element}   element   滑动的页面
   * @param {number}    offset    滑动距离
   * @param {number}    speed     动画持续时间
   * @param {function}  callback  回调函数
   */
  private _translate (element: any, offset: number, speed?: number, callback?: () => any) {
    if (speed) {
      this._animating = true; // 正在执行动画
      transition(element, 'transform ' + speed + 'ms ease-in-out');
      setTimeout(() => {
        transform(element, `translate3d(${offset}px, 0, 0)`);
      }, 50);

      /* 是否已经执行过结束处理, 避免回调执行多次，重复多次逻辑 */
      let called = false;

      let transitionEnd = () => {
        if (called) return;
        called = true;
        this._animating = false; // 动画执行完成
        transition(element, '');
        transform(element, '');
        if (callback) {
          callback.apply(this);
        }
      }

      // 添加动画结束处理
      once(element, 'transitionEnd', transitionEnd);
      once(element, 'webkitTransitionEnd', transitionEnd);
      once(element, 'mozTransitionEnd', transitionEnd);
      once(element, 'msTransitionEnd', transitionEnd);
      // 为一些低版本不兼容或不执行 transitionEnd 的浏览器备用处理, 在结束后 + 100 避免出现同时调用
      setTimeout(transitionEnd, speed + 100);

    } else {
      transition(element, '');
      transform(element, `translate3d(${offset}px, 0, 0)`);
    }
  }

  private _touchStart (point: Point) {
    if (this._noDrag) return; // 如果不需要滑动的时候，则直接跳过滑动处理

    let dragState = this._dragState;

    dragState.startLeft = point.pageX;

    dragState.pageWidth = this._el.offsetWidth;

    let pages = this._pages;
    let index = this._index;
    let prevPage: any = null;
    let dragPage = pages[index];
    let nextPage: any = null;
    let pagesCount = this._length;

    if (index > 0) {
      prevPage = pages[index - 1]
    }
    if (index < pagesCount - 1) {
      nextPage = pages[index + 1]
    }

    // 循环滚动
    if (this._config.continuous && pagesCount > 1) {
      if (!prevPage) {
        prevPage = pages[pagesCount - 1];
      }
      if (!nextPage) {
        nextPage = pages[0];
      }
    }

    if (prevPage) {
      prevPage.style.display = 'block';
    }
    if (nextPage) {
      nextPage.style.display = 'block';
    }

    dragState.prevPage = prevPage;
    dragState.dragPage = dragPage;
    dragState.nextPage = nextPage;
  }

  private _touchMove(point: Point) {
    if (this._noDrag) return;
    let dragState = this._dragState;

    dragState.speedX = point.pageX - dragState.currentLeft;
    dragState.currentLeft = point.pageX;

    // 水平方向的滑动距离
    let offsetLeft = dragState.currentLeft - dragState.startLeft;

    // 最大滑动距离为一个屏幕
    offsetLeft = Math.min(Math.max(-dragState.pageWidth + 1, offsetLeft), dragState.pageWidth - 1);

    let otherOffset = 0;
    // 执行滑动
    if (dragState.prevPage) {
      otherOffset = offsetLeft >= 0 ? (offsetLeft - dragState.pageWidth) : -dragState.pageWidth;
      this._translate(dragState.prevPage, otherOffset);
    }
    this._translate(dragState.dragPage, offsetLeft);
    if (dragState.nextPage) {
      otherOffset = offsetLeft <= 0 ? (offsetLeft + dragState.pageWidth) : dragState.pageWidth;
      this._translate(dragState.nextPage, otherOffset);
    }
  }

  private _touchEnd() {
    if (this._noDrag) return;

    let dragState = this._dragState;

    if (dragState.currentLeft === undefined) {
      this._restoreDom();
      return;
    };

    // 滑动时间
    let towards: string|null = null;
    let offsetLeft = dragState.currentLeft - dragState.startLeft;
    let pageWidth = dragState.pageWidth;
    let index = this._index;
    let pageCount = this._pages.length;

    // 只有手动滑动的距离超过屏幕一半后，才自动滑动到下一页
    if (Math.abs(offsetLeft) > pageWidth / 10) {
      towards = offsetLeft < 0 ? 'next' : 'prev';
    }
    // 如果是不循环播放，则最后一页和首页不允许滑动
    if (!this._config.continuous) {
      if ((index === 0 && towards === 'prev') || index === pageCount - 1 && towards === 'next') {
        towards = null;
      }
    }

    if (pageCount < 2) {
      towards = null;
    }

    this._doAnimate(towards, {
      offsetLeft,
      pageWidth,
      raf: true,
      prevPage: dragState.prevPage,
      currentPage: dragState.dragPage,
      nextPage: dragState.nextPage
    });
    this._dragState = {};
  }
}

export default Carousel;
